package org.wheel.jpa.utils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
import java.util.function.Function;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.metamodel.SingularAttribute;

import org.apache.commons.lang3.StringUtils;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.CollectionUtils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.util.TypeUtils;

/**
 * jpa 通用查询条件
 *
 * @author klaus.wang
 */
public class SpecificationUtils {

	public static final String LIKE = "Like";

	public static final String GT = "Gt";

	public static final String GTEQ = "GtEq";

	public static final String LT = "Lt";

	public static final String LTEQ = "LtEq";

	public static final String IN = "s";

	public static final String PERCENT = "%";

	public static final String LOGIC = "logic";

	public static final String KEY = "key";

	public static final String ITEMS = "items";

	public static final String LOGIC_CONDITION = LOGIC + "Condition";

	/**
	 * 类型转换函数
	 */
	static final Map<Class<?>, Function<Object, Object>> converters;

	/**
	 *
	 */
	static final Map<Class<?>, Map<String, BiFunction<CriteriaBuilder, Object, Expression<?>>>> map;

	static {
		map = new ConcurrentHashMap<>();
		converters = new ConcurrentHashMap<>();
	}

	/**
	 * 组装jpa 条件
	 *
	 * @param parameter
	 * @param <T>
	 * @return
	 */
	public static <T> Specification<T> getPredicate(Object parameter) {
		return (root, query, cb) -> {
			JSONObject params = (JSONObject) JSON.toJSON(parameter);
			Map<String, BiFunction<CriteriaBuilder, Object, Expression<?>>> fn = SpecificationUtils
					.getFunctionMap(root);
			return Optional.ofNullable(params.getJSONObject(LOGIC_CONDITION))
					.map(json -> SpecificationUtils.getPredicate(json, cb, params, fn)).orElseGet(() -> {
						List<Predicate> predicates = new ArrayList<>();
						params.entrySet().forEach(entry -> {
							Object value = entry.getValue();
							if (value == null) {
								return;
							}
							if (value instanceof String && StringUtils.isBlank(String.valueOf(value))) {
								return;
							}
							Optional.ofNullable(fn.get(entry.getKey()))
									.ifPresent(e -> predicates.add((Predicate) e.apply(cb, value)));
						});
						return query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
					});

		};
	}

	@SuppressWarnings("unchecked")
	public static Map<String, BiFunction<CriteriaBuilder, Object, Expression<?>>> getFunctionMap(Root<?> root) {
		return map.computeIfAbsent(root.getJavaType(), (e) -> {
			Set<?> attrs = root.getModel().getSingularAttributes();
			Map<String, BiFunction<CriteriaBuilder, Object, Expression<?>>> funcMap = new HashMap<>(attrs.size());
			for (Object object : attrs) {
				SingularAttribute<?, ?> attr = (SingularAttribute<?, ?>) object;
				String name = attr.getName();
				Function<Object, Object> convert = converters.computeIfAbsent(attr.getJavaType(),
						(t) -> (v) -> t == String.class ? v : TypeUtils.castToJavaBean(v, t));
				if (attr.getJavaType() == String.class) {
					funcMap.put(name + LIKE, (c, v) -> c.like(root.get(name),
							new StringBuffer(PERCENT).append(v).append(PERCENT).toString()));
					funcMap.put(name + "lLike",
							(c, v) -> c.like(root.get(name), new StringBuffer().append(v).append(PERCENT).toString()));
					funcMap.put(name + "RLike",
							(c, v) -> c.like(root.get(name), new StringBuffer(PERCENT).append(v).toString()));
					funcMap.put(name + "NLike", (c, v) -> c.notLike(root.get(name),
							new StringBuffer(PERCENT).append(v).append(PERCENT).toString()));
				} else {
					funcMap.put(name + GT,
							(c, v) -> c.greaterThan(root.get(name), Comparable.class.cast(convert.apply(v))));
					funcMap.put(name + "GtOrEq",
							(c, v) -> c.greaterThanOrEqualTo(root.get(name), Comparable.class.cast(convert.apply(v))));
					funcMap.put(name + LT,
							(c, v) -> c.lessThan(root.get(name), Comparable.class.cast(convert.apply(v))));
					funcMap.put(name + "LtOrEq",
							(c, v) -> c.lessThanOrEqualTo(root.get(name), Comparable.class.cast(convert.apply(v))));
				}
				funcMap.put(name, (c, v) -> c.equal(root.get(name), convert.apply(v)));
				funcMap.put(name + "NE", (c, v) -> c.notEqual(root.get(name), convert.apply(v)));
				funcMap.put(name + IN, (c, v) -> root.get(name).in(convert.apply(v)));
			}
			return funcMap;
		});

	}

	/**
	 * 复杂条件
	 *
	 * @param logicJson
	 * @param cb
	 * @param params
	 * @param fn
	 * @return eg: { logic: 'and', items: [{ key: 'parentId' }, { logic: 'or',
	 *         items: [{ key: 'nameLike' }, { key: 'sourceLike' }] }] }
	 */
	public static Predicate getPredicate(JSONObject logicJson, CriteriaBuilder cb, JSONObject params,
			Map<String, BiFunction<CriteriaBuilder, Object, Expression<?>>> fn) {
		JSONArray items = logicJson.getJSONArray(ITEMS);
		if (CollectionUtils.isEmpty(items)) {
			return null;
		}
		List<Expression<?>> list = new ArrayList<>(items.size());
		for (int i = 0; i < items.size(); i++) {
			Object item = items.get(i);
//			JSONObject json = items.getJSONObject(i);
			if (item instanceof JSONObject && ((JSONObject)item).containsKey(LOGIC)) {
				Predicate predicate = getPredicate((JSONObject)item, cb, params, fn);
				if (predicate != null) {
					list.add(predicate);
				}
			} else {
				String key = item instanceof JSONObject ?
						((JSONObject)item).getString(KEY) : item.toString();
				if (StringUtils.isBlank(key)) {
					continue;
				}
				if (fn.containsKey(key)) {
					Object value = params.get(key);
					if (value == null) {
						continue;
					}
					if (value instanceof String && StringUtils.isBlank(String.valueOf(value))) {
						continue;
					}
					list.add(fn.get(key).apply(cb, value));
				}
			}
		}
		return CollectionUtils.isEmpty(list) ? null : getLogicFunc(cb, (String) logicJson.getOrDefault(LOGIC, one))
				.apply(list.toArray(new Predicate[list.size()]));
	}

	static final String AND = "and";
	static final String one = "1";

	private static Function<Predicate[], Predicate> getLogicFunc(CriteriaBuilder cb, String logic) {
		switch (logic) {
		case one:
		case AND:
			return cb::and;
		default:
			return cb::or;
		}
	}

}
